/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Xml.XPath;
using System.Drawing.Design;
using System.ComponentModel;
using System.Windows.Forms.Design;
using System.Windows.Forms;

namespace DaveSexton.DocProject.Sandcastle.BuildComponents
{
	public sealed class SandcastleBuildComponent : MarshalByRefObject, INotifyPropertyChanged, IXPathNavigable
	{
		#region Public Properties
		public int LineNumber
		{
			get
			{
				return lineNumber;
			}
		}

		public int Index
		{
			get
			{
				return index;
			}
			internal set
			{
				if (index != value)
				{
					index = value;
					OnPropertyChanged(new PropertyChangedEventArgs("Index"));
				}
			}
		}

		public bool IsNew
		{
			get
			{
				return isNew;
			}
			internal set
			{
				isNew = value;
			}
		}

		public Type ComponentType
		{
			get
			{
				return componentType;
			}
		}

		public string ConfigurationFile
		{
			get
			{
				return configurationFile;
			}
		}

		public HelpTypes ConfigurationHelpTypes
		{
			get
			{
				return helpTypes;
			}
		}

		public IXPathNavigable Configuration
		{
			get
			{
				return root;
			}
		}

		internal string ConfigurationAsString
		{
			get
			{
				if (root != null)
					return root.OuterXml;
				else
					return null;
			}
			set
			{
				SaveConfiguration(root, value);

				OnPropertyChanged(new PropertyChangedEventArgs("ConfigurationAsString"));
			}
		}

		public UITypeEditor Editor
		{
			get
			{
				if (editor == null && attributes.HasEditor)
					editor = (UITypeEditor) Activator.CreateInstance(attributes.EditorType, true);

				return editor;
			}
		}

		public string Id
		{
			get
			{
				if (root == null)
					return null;

				string id = root.GetAttribute("id", "");

				if (string.IsNullOrEmpty(id))
					return null;
				else
					return id;
			}
		}

		public string CommentDescription
		{
			get
			{
				return commentDescriptionBuffer ?? commentDescription;
			}
			set
			{
				if (value == null)
					value = string.Empty;

				if (!string.Equals(commentDescriptionBuffer, value, StringComparison.Ordinal))
				{
					commentDescriptionBuffer = value;

					OnPropertyChanged(new PropertyChangedEventArgs("CommentDescription"));

					if (!attributes.HasDescription)
					{
						description = commentDescriptionBuffer;

						OnPropertyChanged(new PropertyChangedEventArgs("Description"));
					}
				}
			}
		}

		public string Description
		{
			get
			{
				return description;
			}
		}

		public string Assembly
		{
			get
			{
				return assemblyPath;
			}
		}

		public string SimpleTypeName
		{
			get
			{
				return componentType.Name;
			}
		}

		public string TypeName
		{
			get
			{
				return typeName;
			}
		}

		public string UniqueName
		{
			get
			{
				return TypeName + Index;
			}
		}

		public string Name
		{
			get
			{
				return name;
			}
		}

		public SandcastleBuildComponentAttributes Attributes
		{
			get
			{
				return attributes;
			}
		}

		public bool IsDirty
		{
			get
			{
				return commentDescriptionBuffer != null || !string.Equals(root.OuterXml, committedValue, StringComparison.Ordinal);
			}
		}

		public bool IsBuiltInSandcastleComponent
		{
			get
			{
				return isBuiltInSandcastleComponent;
			}
		}

		public SandcastleBuildComponentManager Manager
		{
			get
			{
				return manager;
			}
		}
		#endregion

		#region Private / Protected
		internal Exception LoadException
		{
			get
			{
				return loadException;
			}
		}

		private int index;
		private bool isNew;
		private string commentDescriptionBuffer, commentDescription, description, committedValue;
		private UITypeEditor editor;
		private readonly HelpTypes helpTypes;
		private readonly bool isBuiltInSandcastleComponent;
		private readonly int lineNumber;
		private readonly string assemblyPath, typeName, name, configurationFile;
		/// <summary>
		/// Always use the root instead of the <see cref="document"/> field since writing to both at different times causes 
		/// whitespace to be lost and externally, <see cref="IXPathNavigable"/> is always used.
		/// </summary>
		private readonly XPathNavigator root;
		private readonly XmlDocument document;
		private readonly Type componentType;
		private readonly Exception loadException;
		private readonly SandcastleBuildComponentAttributes attributes;
		private readonly SandcastleBuildComponentManager manager;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="SandcastleBuildComponent" /> class.
		/// </summary>
		internal SandcastleBuildComponent(SandcastleBuildComponentManager manager, string configurationFile, HelpTypes helpTypes, int index, Type componentType, XmlNode node, bool isNew)
		{
			this.manager = manager;
			this.configurationFile = configurationFile;
			this.index = index;
			this.lineNumber = -1;
			this.isNew = isNew;

			commentDescription = string.Empty;

			document = new XmlDocument();
			document.PreserveWhitespace = true;
			document.AppendChild(document.ImportNode(node, true));

			if (document.DocumentElement != null)
			{
				root = document.DocumentElement.CreateNavigator();

				committedValue = root.OuterXml;
			}

			description = null;	// set later in this method

			this.componentType = componentType;
			this.typeName = componentType.FullName;
			this.assemblyPath = componentType.Assembly.CodeBase;
			this.helpTypes = helpTypes;

			isBuiltInSandcastleComponent = componentType.Assembly == typeof(Microsoft.Ddue.Tools.SaveComponent).Assembly;

			attributes = new SandcastleBuildComponentAttributes(this, componentType);

			description = attributes.Description;

			if (string.IsNullOrEmpty(description))
				description = commentDescription;

			name = attributes.DisplayName;

			if (string.IsNullOrEmpty(name))
				name = Id ?? componentType.Name;

			// NOTE: This method must be called after the description and name fields have been initialized
			attributes.EnsurePropertyGridAttributes();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="SandcastleBuildComponent" /> class.
		/// </summary>
		internal SandcastleBuildComponent(SandcastleBuildComponentManager manager, string configurationFile, HelpTypes helpTypes, int index, int lineNumber, string commentDescription, string configuration)
		{
			this.manager = manager;
			this.configurationFile = configurationFile;
			this.index = index;
			this.lineNumber = lineNumber;
			this.commentDescription = commentDescription ?? string.Empty;
			this.helpTypes = helpTypes;

			document = new XmlDocument();
			document.PreserveWhitespace = true;
			document.LoadXml(configuration);

			if (document.DocumentElement != null)
			{
				root = document.DocumentElement.CreateNavigator();

				committedValue = root.OuterXml;

				typeName = root.GetAttribute("type", "");
				assemblyPath = Environment.ExpandEnvironmentVariables(root.GetAttribute("assembly", ""));
			}

			System.Reflection.Assembly assembly = System.Reflection.Assembly.LoadFrom(assemblyPath);

			// NOTE: This is required so that Type.GetType() will return the component's Editor and TypeConverter, 
			// if they are assigned, when used by the PropertyDescriptor class or this class's Editor property.  
			// (Type.GetType() requires the "Load" context but the assembly is only being loaded into the "LoadFrom" 
			// context above.)
			AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs e)
			{
				if (string.Equals(e.Name, assembly.FullName, StringComparison.OrdinalIgnoreCase))
					return assembly;
				else
					return null;
			};

			componentType = assembly.GetType(typeName, true, false);

			isBuiltInSandcastleComponent = assembly == typeof(Microsoft.Ddue.Tools.SaveComponent).Assembly;

			attributes = new SandcastleBuildComponentAttributes(this, componentType);

			description = attributes.Description;

			if (string.IsNullOrEmpty(description))
				description = commentDescription;

			name = attributes.DisplayName;

			if (string.IsNullOrEmpty(name))
				name = Id ?? componentType.Name;

			// NOTE: This method must be called after the description and name fields have been initialized
			attributes.EnsurePropertyGridAttributes();
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="SandcastleBuildComponent" /> class.
		/// </summary>
		internal SandcastleBuildComponent(SandcastleBuildComponentManager manager, string configurationFile, HelpTypes helpTypes, Exception loadException, int index, int lineNumber, string commentDescription)
		{
			this.manager = manager;
			this.configurationFile = configurationFile;
			this.index = index;
			this.lineNumber = lineNumber;
			this.loadException = loadException;
			this.commentDescription = commentDescription ?? string.Empty;
			this.helpTypes = helpTypes;

			description = loadException.Message;
			name = loadException.GetType().Name;

			assemblyPath = typeName = string.Empty;

			attributes = new SandcastleBuildComponentAttributes(this, "Load error", description, typeof(Editors.DisplayLoadExceptionEditor), true);
		}
		#endregion

		#region Methods
		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		internal static void SaveConfiguration(IXPathNavigable target, string config)
		{
			if (config == null)
				return;

			// create a temporary element only used for retrieving the InnerXml
			XPathNavigator updated = new XmlDocument().CreateElement("root").CreateNavigator();

			updated.InnerXml = config;

			XPathNavigator navigator = target.CreateNavigator();

			// NOTE: write to the DOM so that whitespace is preserved.
			// Mixing the XPathNavigator and DOM models together causes whitespace to be lost.  Similarly, even when using only XPathNavigators, you can assign the InnerXml property 
			// of an "inner" navigator to XML that contains insignifact whitespace and it can be read back from a navigator at the same position, but not any "outer" navigators 
			// (the XML is there, of course, but none of the whitespace is preserved).  So the solution to this problem is to only read/write finalized XML to the DOM and not to 
			// XPathNavigator, although it may be used intermittently since it will keep track of whitespace written to itself when used outside of this component.
			XmlNode node = (XmlNode) navigator.UnderlyingObject;

			if (!updated.MoveToFirstChild())
				node.InnerXml = string.Empty;
			else
			{
				node.InnerXml = updated.InnerXml;

				XPathNavigator clone = navigator.Clone();

				if (clone.MoveToFirstChild() && clone.NodeType == XPathNodeType.Element)
					node.InnerXml = Environment.NewLine + node.InnerXml;

				DeleteCustomAttributes(node);

				if (updated.MoveToFirstAttribute())
				{
					do
					{
						string name = updated.Name;

						if (!string.Equals(updated.Prefix, "xml", StringComparison.Ordinal)
							&& !string.Equals(name, "assembly", StringComparison.Ordinal)
							&& !string.Equals(name, "type", StringComparison.Ordinal))
						{
							if (navigator.MoveToAttribute(name, ""))
							{
								navigator.SetValue(updated.Value);
								navigator.MoveToParent();
							}
							else
								navigator.CreateAttribute(null, name, null, updated.Value);
						}
					}
					while (updated.MoveToNextAttribute());
				}
			}
		}

		private static void DeleteCustomAttributes(XmlNode node)
		{
			List<XmlAttribute> attributes = new List<XmlAttribute>(4);

			foreach (XmlAttribute attribute in node.Attributes)
				if (string.Equals(attribute.Prefix, "xml", StringComparison.Ordinal)
					|| string.Equals(attribute.Name, "assembly", StringComparison.Ordinal)
					|| string.Equals(attribute.Name, "type", StringComparison.Ordinal))
					attributes.Add(attribute);

			node.Attributes.RemoveAll();

			foreach (XmlAttribute attribute in attributes)
				node.Attributes.Append(attribute);
		}

		public void CommitChanges()
		{
			System.Diagnostics.Debug.WriteLine("Committing changes to build component: " + name + " (" + typeName + ")");

			if (commentDescriptionBuffer != null)
				commentDescription = commentDescriptionBuffer;

			committedValue = Configuration.CreateNavigator().OuterXml;

			commentDescriptionBuffer = null;
		}

		public void CancelChanges()
		{
			System.Diagnostics.Debug.WriteLine("Canceling changes to build component: " + name + " (" + typeName + ")");

			root.OuterXml = committedValue;

			commentDescriptionBuffer = null;
		}

		public bool CanDisplayOrEdit(ITypeDescriptorContext context)
		{
			return loadException != null || attributes.HasEditorOnType || HasProperties(context);
		}

		public bool HasProperties(ITypeDescriptorContext context)
		{
			if (!attributes.HasTypeConverter)
				return false;
			else
			{
				TypeConverter converter = (attributes.HasTypeConverter)
					? (TypeConverter) Activator.CreateInstance(attributes.ConverterType, true)
					: TypeDescriptor.GetConverter(componentType);

				return converter != null && converter.GetPropertiesSupported(context);
			}
		}
		#endregion

		#region INotifyPropertyChanged Members
		private readonly object PropertyChangedEventLock = new object();
		private PropertyChangedEventHandler PropertyChangedEvent;

		/// <summary>
		/// Event raised when a property value changes.
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged
		{
			add
			{
				lock (PropertyChangedEventLock)
				{
					PropertyChangedEvent += value;
				}
			}
			remove
			{
				lock (PropertyChangedEventLock)
				{
					PropertyChangedEvent -= value;
				}
			}
		}

		/// <summary>
		/// Raises the <see cref="PropertyChanged" /> event.
		/// </summary>
		/// <param name="e"><see cref="PropertyChangedEventArgs" /> object that provides the arguments for the event.</param>
		private void OnPropertyChanged(PropertyChangedEventArgs e)
		{
			PropertyChangedEventHandler handler = null;

			lock (PropertyChangedEventLock)
			{
				handler = PropertyChangedEvent;
			}

			if (handler != null)
				handler(this, e);
		}
		#endregion

		#region IXPathNavigable Members
		public XPathNavigator CreateNavigator()
		{
			return Configuration.CreateNavigator();
		}
		#endregion
	}
}
